协程中的取消和异常 | 取消操作详解
调用 cancel 方法
// 假设我们已经定义了一个作用域
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
取消作用域会取消它的子协程
// 假设我们已经定义了一个作用域
val job1 = scope.launch { … }
val job2 = scope.launch { … }
// 第一个协程将会被取消,而另一个则不受任何影响
job1.cancel()
被取消的子协程并不会影响其余兄弟协程
fun cancel(cause: CancellationException? = null)
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}
完整代码 https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/JobSupport.kt#L612
viewModelScope
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope
lifecycleScope
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope
为什么协程处理的任务没有停止?
如果我们仅是调用了 cancel 方法,并不意味着协程所处理的任务也会停止。如果您使用协程处理了一些相对较为繁重的工作,比如读取多个文件,那么您的代码不会自动就停止此任务的进行。
让我们举一个更简单的例子看看会发生什么。假设我们需要使用协程来每秒打印两次 "Hello"。我们先让协程运行一秒,然后将其取消。其中一个版本实现如下所示:
Hello 0
Hello 1
Hello 2
让您的协程可以被取消
您需要确保所有使用协程处理任务的代码实现都是协作式的,也就是说它们都配合协程取消做了处理,因此您可以在任务处理期间定期检查协程是否已被取消,或者在处理耗时任务之前就检查当前协程是否已取消。例如,如果您从磁盘中获取了多个文件,在开始读取文件内容之前,先检查协程是否被取消了。类似这样的处理方式,您可以避免处理不必要的 CPU 密集型任务。
val job = launch {
for(file in files) {
// TODO 检查协程是否被取消
readFile(file)
}
}
检查 job.isActive 或者使用 ensureActive() 使用 yield() 来让其他任务进行
检查 job 的活跃状态
// 因为处于 launch 的代码块中,可以访问到 job.isActive 属性
while (i < 5 && isActive)
Coroutine 的代码库中还提供了另一个很有用的方法 —— ensureActive(),它的实现如下:
fun Job.ensureActive(): Unit {
if (!isActive) {
throw getCancellationException()
}
}
如果 job 处于非活跃状态,这个方法会立即抛出异常,我们可以在 while 循环开始就使用这个方法。
while (i < 5) {
ensureActive()
…
}
通过使用 ensureActive 方法,您可以避免使用 if 语句来检查 isActive 状态,这样可以减少样板代码的使用量,但是相应地也失去了处理类似于日志打印这种行为的灵活性。
使用 yield() 函数运行其他任务
yield()
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
Job.join 🆚 Deferred.await cancellation
如果您调用 job.cancel 之后再调用 job.join,那么协程会在任务处理完成之前一直处于挂起状态; 在 job.join 之后调用 job.cancel 没有什么影响,因为 job 已经完成了。
Job.join
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
Deferred
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
Deferred.await
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
val deferred = async { … }
deferred.cancel()
val result = deferred.await() // 抛出 JobCancellationException 异常
处理协程取消的副作用
假设您要在协程取消后执行某个特定的操作,比如关闭可能正在使用的资源,或者是针对取消需要进行日志打印,又或者是执行其余的一些清理代码。我们有好几种方法可以做到这一点:
检查 !isActive
如果您定期地进行 isActive 的检查,那么一旦您跳出 while 循环,就可以进行资源的清理。之前的代码可以更新至如下版本:
while (i < 5 && isActive) {
if (…) {
println(“Hello ${i++}”)
nextPrintTime += 500L
}
}
// 协程所处理的任务已经完成,因此我们可以做一些清理工作
println(“Clean up!”)
完整版本
https://pl.kotl.in/loI9DaIYj
Try catch finally
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
println(“Clean up!”)
}
}
delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)
完整代码 https://pl.kotl.in/wjPINnWfG
当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于 NonCancellable CoroutineContext 中。这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
withContext(NonCancellable){
delay(1000L) // 或一些其他的挂起函数
println(“Cleanup done!”)
}
}
}
delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)
工作原理 https://pl.kotl.in/ufZRQSa7o
suspendCancellableCoroutine 和 invokeOnCancellation
如果您通过 suspendCoroutine 方法将回调转为协程,那么您更应该使用 suspendCancellableCoroutine 方法。可以使用 continuation.invokeOnCancellation 来执行取消操作:
suspend fun work() {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
// 处理清理工作
}
// 剩余的实现代码
}
为了享受到结构化并发带来的好处,并确保我们并没有进行多余的操作,那么需要保证代码是可被取消的。
使用在 Jetpack: viewModelScope 或者 lifecycleScope 中定义的 CoroutineScopes,它们在 scope 完成后就会取消它们处理的任务。如果要创建自己的 CoroutineScope,请确保将其与 job 绑定并在需要时调用 cancel。
协程代码的取消需要是协作式的,因此请将代码更新为对协程的取消操作以延后的方式进行检查,并避免不必要的操作。
现在,大家了解了本系列的第一部分协程的一些基本概念、第二部分协程的取消,在接下来的文章中,我们将继续深入探讨学习第三部分异常处理,感兴趣的读者请继续关注我们的更新。
推荐阅读